#!/usr/bin/python2.7

import os
import xml.etree.ElementTree
import itertools
import copy
import shutil
import datetime
import fnmatch

debug = True

################################################################

class mindmap_t(): # {{{
    """
    .root = node_t()
    """
    def __init__(self, mindmap_fpath):
        self.fpath = mindmap_fpath
        assert os.path.isfile(self.fpath), 'Error: cannot find mindmap file ' + self.fpath

        self.tree = xml.etree.ElementTree.parse(self.fpath)
        self.elem = self.tree.getroot()
        assert self.elem.tag == 'map', self.elem.tag

        n = self.elem.find('node')
        assert n is not None
        self.root = node_t(n, 0, None) # TODO: probably this will cause floating node cannot be parsed

        self.ls_change = list()

    def str(self):
        s = ''
        for n in iter(self.root):
            indent = '  ' * n.level
            s += indent + n.core.replace('\n', '\\n')
        return s.encode('utf-8')

# }}}

class node_t(): # {{{
    """
    .core       = string
    .details    = string
    .note       = string
    .attribute  = dict
    .style_ref  = string
    .link       = string
    .ls_icon    = list(string)
    .ls_sub     = list(node_t)

    .format     = string

    .id
    .created
    .modified

    .level      = int
    .parent     = node_t
    """
    def __init__(self, elem, level, parent): # {{{
        assert elem.tag == 'node', elem.tag

        self.elem       = elem

        self.id         = elem.get('ID', default='')
        self.created    = elem.get('CREATED', default='')
        self.modified   = elem.get('MODIFIED', default='')
        self.level      = level
        self.parent     = parent

        self.style_ref  = elem.get('STYLE_REF', default='')
        self.link       = elem.get('LINK', default='')

        self.core       = elem.get('TEXT', default='')
        self.details    = ''
        self.note       = ''

        #-------------------------------------------------------
        # process richcontent
        for sub_elem_richcontent in elem.findall('richcontent'):
            # process the html of richcontent, only support plain text style in <p> </p>
            # TODO: support ul/ol
            # TODO: support table
            text = ''
            for p in sub_elem_richcontent.iter(tag='p'):
                text += p.text.strip() + '\n'

            # different types of richcontent
            type = sub_elem_richcontent.get('TYPE')
            if (type == 'NODE'):
                self.core = text
            elif (type == 'DETAILS'):
                self.details = text
            elif (type == 'NOTE'):
                self.note = text
            else:
                raise RuntimeError, 'Error: richcontent type (%s) is not recognized' % type

        #-------------------------------------------------------
        # process attribute
        self.attribute  = dict()

        for sub_elem_attribute in elem.findall('attribute'):
            self.attribute[sub_elem_attribute.get('NAME')] = sub_elem_attribute.get('VALUE')

        #-------------------------------------------------------
        # process icon
        self.ls_icon = list()
        for sub_elem_icon in elem.findall('icon'):
            self.ls_icon.append(sub_elem_icon.get('BUILTIN'))

        #-------------------------------------------------------
        # process sub node
        self.ls_sub = list()

        for sub_elem_node in elem.findall('node'):
            self.ls_sub.append(node_t(sub_elem_node, self.level+1, self))

        #-------------------------------------------------------
        # post-process
        self.format = self.style_ref
    # }}}

    def __iter__(self):
        yield self
        for n in itertools.chain(*itertools.imap(iter, self.ls_sub)):
            yield n

    def __eq__(self, other):
        return self.id == other.id

# }}}


################################################################
# blog mode
################################################################

blog_mode_footnote = '~~~\nThis Markdown file is generated by freeplane.py blog mode https://github.com/phdbreak/mindmap.py\n~~~'

def mindmap_to_blog(mindmap, do_cleanup=False):
    #===========================================================
    # cleanup previous markdown files
    #===========================================================
    # {{{
    if do_cleanup:
        print '\n## cleanup old markdown files created by freeplane.py blog mode'
        dpath = os.path.dirname(mindmap.fpath)
        for root, ls_dname, ls_fname in os.walk(dpath):
            for fname in ls_fname:
                if (fname.endswith('.md')):
                    # is Markdown file
                    fpath = os.path.join(root, fname)
                    with open(fpath, 'r') as f:
                        f.seek(-2 * len(blog_mode_footnote), os.SEEK_END)
                        last = f.read().strip()
                    if last.endswith(blog_mode_footnote):
                        # is generated, so remove the markdown file
                        print '### remove file ' + fpath
                        os.remove(fpath)

    # }}}

    #===========================================================
    # find blog mode settings
    #===========================================================
    # {{{

    print '\n## analyze blog mode settings'
    setting_root = next(itertools.ifilter(lambda x: x.core.lower() == 'blog mode settings', mindmap.root.ls_sub), None)
    assert (setting_root != None), 'Error: cannot find "blog mode settings" node'

    #-------------------------------------------------------
    # node type & link type

    node_type = dict()
    link_type = dict()
    for node in itertools.islice(iter(setting_root), 1, None):
        type_name = node.core.lower()

        if (type_name == 'special nodes'):
            continue

        if (type_name == 'link file types'):
            for (keyword, value) in node.attribute.items():
                link_type[keyword] = value.split()
            continue

        try:
            node_type[type_name].append(node)
        except KeyError:
            node_type[type_name] = [node]

    print '### node type'
    for (type_name, ls_node) in node_type.items():
        s = '%s = ' % type_name
        for node in ls_node:
            s += node.format + '|'
        print s.strip('|')

    print '### link type'
    for (type_name, ls_pattern) in link_type.items():
        s = '%s = ' % type_name
        for pattern in ls_pattern:
            s += pattern + '|'
        print s.strip('|')

    #-------------------------------------------------------
    # article's default meta-data

    article_setting = next(itertools.ifilter(lambda n: n.core.lower() == 'article', setting_root), None)
    assert article_setting != None, 'Error: cannot find "article" setting node'
    default_metadata = dict()
    for (key, value) in article_setting.attribute.items():
        default_metadata[key.lower()] = value

    print '### default meta-data'
    for (key, value) in default_metadata.items():
        print '%s = %s' % (key, value)
    # }}}

    #===========================================================
    # set node type according to their format matching with blog mode settings
    #===========================================================
    # {{{

    print '\n## match node format'

    # node_type
    for node in iter(mindmap.root):
        node.type = ''
        for (type_name, ls_node_setting) in node_type.items():
            for node_setting in ls_node_setting:
                if (node.format == node_setting.format):
                    node.type = type_name
                    break
            if len(node.type) > 0:
                break
        if (node.type == ''):
            if node.core.startswith('- '):
                node.type = 'list'
                if (node.parent.type != 'list'):
                    node.list_level = 1
                else:
                    node.list_level = node.parent.list_level + 1

    for node in iter(setting_root):
        node.type = ''
    mindmap.root.type = ''

    # link_type
    for node in itertools.ifilter(lambda n: len(n.link) > 0, mindmap.root):
        node.link_type = ''
        for (type_name, ls_link_type_pattern) in link_type.items():
            for link_type_pattern in ls_link_type_pattern:
                if (fnmatch.fnmatch(node.core, link_type_pattern)):
                    node.link_type = type_name
                    break
            if len(node.link_type) > 0:
                break
    # }}}

    #===========================================================
    # analyze article
    #===========================================================
    # {{{

    print '\n## analyze blog articles'

    for node in itertools.ifilter(lambda n: n.type == 'article', mindmap.root):
        print '\n### analyze article:\n' + node.core.encode('utf-8')

        #-------------------------------------------------------
        # article file path
        dpath = os.path.dirname(mindmap.fpath)
        if node.parent.type == 'directory':
            directory = node.parent.core.replace(' ', '-').replace(':', '..')
            dpath = os.path.join(dpath, directory)

        #-------------------------------------------------------
        # article meta-data
        metadata = dict()
        for (key, value) in node.attribute.items():
            metadata[key.lower()] = value
        metadata['title'] = node.core
        metadata['category'] = directory.replace(' ', '-')
        if ('slug' not in metadata.keys()):
            metadata['slug'] = metadata['title'].lower().replace(' ', '-')

        # set default meta-data
        for (keyword, value) in default_metadata.items():
            if keyword not in metadata.keys():
                metadata[keyword] = value

        if ('modified' not in metadata.keys()) or (metadata['modified'] == ''):
            if ('created' in metadata.keys()):
                metadata['modified'] = metadata['created']
        if ('created' in metadata.keys()):
            metadata['date'] = metadata['created']

        fname = metadata['slug'] + '.md'
        fpath = os.path.join(dpath, fname)

        #-------------------------------------------------------
        # write article
        if not os.path.isdir(dpath):
            os.mkdir(dpath)

        # title
        content = 'title: ' + metadata['title'] + '\n'

        # meta-data
        for (key, value) in metadata.items():
            if (key == 'title'):
                continue
            content += '%s: %s\n' % (key, value)
        content += '\n'

        #-------------------------------------------------------
        # sub node content {{{
        for n in itertools.islice(iter(node), 1, None):
            if n.type == 'skip':
                continue

            if n.type == 'comment':
                map(lambda m: setattr(m, 'type', 'comment'), n.ls_sub)
                continue

            # deal with links
            if len(n.link) > 0:
                # solve relative link
                if n.link.startswith('http://'):
                    # http link
                    link = n.link
                else:
                    # local files
                    # copy local files to directory it supposed to be in: "<directory>/<slug>/", and modify the link in original mindmap file
                    exp_link = directory + '/' + metadata['slug'] + '/' + n.core.replace(' ', '_')
                    if (n.link != exp_link):
                        old_fpath = os.path.join(os.path.dirname(mindmap.fpath), n.link).replace(r'%20', r' ')
                        new_fpath = os.path.join(os.path.dirname(mindmap.fpath), exp_link)
                        if (os.path.dirname(n.link) == os.path.dirname(exp_link)):
                            # just filename is different, rename
                            print 'Info: file (%s) is renamed to (%s)' % (old_fpath, new_fpath)
                            os.rename(old_fpath, new_fpath)
                        else:
                            # copy
                            print 'Info: file (%s) is copied to (%s)' % (old_fpath, new_fpath)
                            if not os.path.isdir(os.path.dirname(new_fpath)):
                                os.mkdir(os.path.dirname(new_fpath))
                            shutil.copyfile(old_fpath, new_fpath)

                        # change the link
                        mindmap.is_changed = True
                        before = 'LINK="' + n.link + '"'
                        after = 'LINK="' + exp_link + '"'
                        mindmap.ls_change.append((before, after))

                    link = metadata['slug'] + '/' + n.core.replace(' ', '_')

                if n.link_type == 'image':
                    content += '![%s](%s)\n' % (n.core, link)
                else:
                    content += '[%s](%s)\n' % (n.core, link)

                continue

            # deal with special nodes
            prefix = ''
            suffix = ''

            if n.type == 'section':
                prefix = '# '
            elif n.type == 'subsection':
                prefix = '## '
            elif n.type == 'subsubsection':
                prefix = '### '
            elif n.type == 'paragraph':
                prefix = '#### '
            elif n.type == 'subparagraph':
                prefix = '##### '
            elif n.type == 'code':
                prefix = '~~~\n'
                suffix = '\n~~~'
            elif n.type == 'list':
                prefix = '    ' * (n.list_level - 1)
                suffix = ''

            content += prefix + n.core + suffix + '\n'

            if len(n.note) > 0:
                content += '\n' + n.note + '\n'
        # }}}

        content += blog_mode_footnote

        #-------------------------------------------------------
        # write the markdown file {{{
        f = open(fpath, 'w')
        print >> f, content.encode('utf-8')
        f.close()
        print '### wrote to ' + fpath
        # }}}

    # }}}

    #===========================================================
    # modify original mindmap and backup
    #===========================================================
    # {{{
    if len(mindmap.ls_change) == 0:
        pass
    else:
        print '\n## change mindmap with backup'

        # backup

        prefix = datetime.datetime.now().strftime('%Y%m%d_%H%M%S.')
        bak_fpath = os.path.join(os.path.dirname(mindmap.fpath), 'bak', prefix + os.path.basename(mindmap.fpath))
        if not os.path.isdir(os.path.dirname(bak_fpath)):
            os.mkdir(os.path.dirname(bak_fpath))
        shutil.copyfile(mindmap.fpath, bak_fpath)
        print '### mindmap file backup at %s' % bak_fpath

        # replace mindmap
        with open(mindmap.fpath, 'r') as f:
            ls_lines = f.readlines()

        for (before, after) in mindmap.ls_change:
            print '### (%s) >> (%s)' % (before, after)
            for (i, line) in enumerate(ls_lines):
                ls_lines[i] = line.replace(before, after)

        with open(mindmap.fpath, 'w') as f:
            f.writelines(ls_lines)
    # }}}

    #===========================================================
    # cleanup not used attachment dir
    #===========================================================
    # {{{
    if (do_cleanup):
        print '\n## cleanup not used attachment dir'
        dpath = os.path.dirname(mindmap.fpath)
        for root, ls_dname, ls_fname in os.walk(dpath):
            # skip the first level
            if (root == dpath) or (os.path.basename(root) == 'bak'):
                continue

            st_dname = set(ls_dname)
            st_slug = set(map(lambda x: x[:-3], ls_fname))
            st_not_used = st_dname - st_slug
            for dname in st_not_used:
                dpath = os.path.join(root, dname)
                print '### remove dir ' + dpath
                shutil.rmtree(dpath)
    # }}}


if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser(description='freeplane.py')
    parser.add_argument('mindmap_file_path')
    parser.add_argument('--blog_mode', action='store_true', default=False)
    parser.add_argument('--blog_mode_cleanup', action='store_true', default=False)
    args = parser.parse_args()

    if (args.blog_mode):
        print '# freeplane.py blog mode: START'
        print 'input = %s' % args.mindmap_file_path
        print ''
        mindmap = mindmap_t(args.mindmap_file_path)
        mindmap_to_blog(mindmap, do_cleanup=args.blog_mode_cleanup)
        print '\n# freeplane.py blog mode: DONE'
